package pub.tbc.mybatis.assist.plugin.page;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * @author tbc on 2017/5/15 16:42:54.
 */
@Slf4j
public abstract class AbstractPagePlugin implements Interceptor {
    private Integer defaultPageNum;
    private Integer defaultPageSize;
    private Boolean defaultUseFlag;
    private Boolean defaultCheckFlag;
    private Boolean defaultCorrectPage;

    {
        init(new Properties());
    }

    /**
     * 从代理对象中分离出真实对象
     *
     * @param invocation .
     * @return 非代理StatementHandler对象
     */
    private StatementHandler getUnProxyObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        // 分离代理对象链（由于目标类可能被多个拦截器拦截，从而形成多次代理，通过循环可以分离出原始的目标类）,
        // JDK动态代理，生成的对象，都是继承了proxy类，实现了指定接口，不能代理类的原因也在此，因此JAVA不支持多继承
        Object object = null;
        while (metaStatementHandler.hasGetter("h"))
            object = metaStatementHandler.getValue("h");

        if (null == object)
            return statementHandler;
        return (StatementHandler) object;
    }

    /**
     * 检查是否是select语句
     *
     * @param sql .
     * @return .
     */
    private boolean checkSelect(String sql) {
        if (sql == null || sql.isEmpty())
            throw new RuntimeException("拿到的SQL怎么是空的");
//        return sql.trim().toLowerCase().startsWith("select ");
        return sql.trim().toLowerCase().indexOf("select") == 0;
    }

    /**
     * 分解分页参数，在这里下面三种方式都是支持使用的
     * 1. Map
     * 2. @Param(或不带此注释)注解传递参数(仍然会被包装成Map<String, Object>)
     * 3. POJO继承PageParams
     *
     * @param parameterObject .
     * @return 分页参数
     */
    @SuppressWarnings("unchecked")
    private PageParams getPageParams(Object parameterObject) {
        if (parameterObject == null)
            return null;
        PageParams pageParams = null;
        //支持Map参数和MyBatis的@Param注解的参数
        if (parameterObject instanceof Map) {
            Map<String, Object> paramMap = (Map) parameterObject;
            Set<String> keys = paramMap.keySet();
            // 循环一遍，如果有对象是PageParams的实例或者子类实例，就是分页参数了，返回去
            for (String k : keys) {
                Object value = paramMap.get(k);
                if (value instanceof PageParams)
                    return (PageParams) value;
            }
        } else if (parameterObject instanceof PageParams) {
            // 如果是PageParams的实例或子类实例
            pageParams = (PageParams) parameterObject;
        }
        return pageParams;
    }

    /**
     * 获取记录总数
     *
     * @param invocation           Invocation
     * @param metaStatementHandler statementHandler
     * @param boundSql             sql
     * @return sql查询到的记录总数
     */
    private int getTotal(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, String sourceSql) throws Exception {
        // 获取当前mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 获取配置对象
        Configuration configuration = mappedStatement.getConfiguration();

        // 改写为统计总数的SQL，这里暂时只支持MYSQL数据库
        String countSql = getCountSql(sourceSql);
        // 获取拦截方法参数， 我们知道是Connection对象[见StatementHandler的prepare方法的参数列表]
        Connection conn = (Connection) invocation.getArgs()[0];
//        PreparedStatement statement = null;
        int total = 0;
        // Try-with-resources @since jdk1.7 自动关闭资源,Connection不能关(否则后续的SQL就没法执行了)，所以定义在外面
        try (PreparedStatement statement = conn.prepareStatement(countSql)) {
            // 构建统计总数BoundSql
            BoundSql countBoundSql = new BoundSql(
                    configuration,
                    countSql,
                    boundSql.getParameterMappings(),
                    boundSql.getParameterObject()
            );
            // 构建MyBatis的ParameterHandler[参数处理器]用来设置countSql的参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(
                    mappedStatement,
                    boundSql.getParameterObject(),
                    countBoundSql
            );
            // 设置countSql的参数
            parameterHandler.setParameters(statement);
            // exec
            ResultSet rs = statement.executeQuery();
            while (rs.next()) {
                total = rs.getInt(1);
            }
        }
        log.debug("总记录数：{}", total);
        return total;
    }

    private void setTotal(PageParams pageParams, Integer total) {
        int pageSize = pageParams.getPageSize();
        // 计算总页数
        int totalPage = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
        // 设置总数及总页数
        pageParams.total(total).totalPage(totalPage);
//        Pages.add(new PageWrapper(pageParams));
    }

    private void checkPage(PageParams pageParams) {
        int pageNum = pageParams.getPageNum();
        if (pageNum <= 0)
            throw new MybatisPagePluginException("分页参数有误，页码必须为大于0的正整数");
        if (pageNum > pageParams.getTotalPage()) {
            // 是否自动变更页码为合理值：最后一页
            if (pageParams.getCorrectPage())
                pageParams.setPageNum(pageParams.getPageSize());
        }
    }

    /**
     * 修改当前查询的SQL，主要是添加分页，仅支持Mysql，使用Limit关键字
     *
     * @param invocation           Invocation
     * @param metaStatementHandler MetaObject
     * @param pageNum              请求的页码
     * @param pageSize             每页数量
     * @return .
     */
    private Object process(
            Invocation invocation,
            MetaObject metaStatementHandler,
            Integer pageNum,
            Integer pageSize,
            String sql
    ) throws InvocationTargetException, IllegalAccessException, SQLException {
        // 回填到对象中，修改要执行的SQL
        metaStatementHandler.setValue("delegate.boundSql.sql", sql);
        // 相当于调用StatementHandler的prepare方法，预编译了当前SQL并设置原有的参数，但是少了两个分页参数，返回一个PreparedStatement
        // throws InvocationTargetException, IllegalAccessException
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        // 计算SQL总参数个数 \\throws SQLException
        int count = ps.getParameterMetaData().getParameterCount();
        // 设置两个分页参数
        ps.setInt(count - 1, (pageNum - 1) * pageSize);
        ps.setInt(count, pageSize);
        return ps;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.debug("Interceptor: {}, invocation: {}", getClass(), invocation);

        StatementHandler statementHandler = getUnProxyObject(invocation);


        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // 获取原始SQL
        String sourceSql = (String) metaObject.getValue("delegate.boundSql.sql");
        // 过滤Select语句
        if (!checkSelect(sourceSql)) {
            return invocation.proceed();
        }

        BoundSql boundSql = statementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        PageParams pageParams = getPageParams(parameterObject);

        // 没有分页参数，不用分页
        if (pageParams == null)
            return invocation.proceed();

        boolean useFlag = pageParams.getUseFlag() == null ? this.defaultUseFlag : pageParams.getUseFlag();

        // 如果设定不启用分页，路过
        if (!useFlag) return invocation.proceed();

        boolean checkFlag = pageParams.getCheckFlag() == null ? this.defaultCheckFlag : pageParams.getCheckFlag();
        Integer pageNum = pageParams.getPageNum() == null ? this.defaultPageNum : pageParams.getPageNum();
        Integer pageSize = pageParams.getPageSize() == null ? this.defaultPageSize : pageParams.getPageSize();


        int total = getTotal(invocation, metaObject, boundSql, sourceSql);
        // 设置总数，并计算总页数，然后回填到分页参数对象
        setTotal(pageParams, total);

        // 检查当前页码的有效性，以及是否自动处理大于总数的页码
        if (checkFlag) checkPage(pageParams);

        // 使用Mybatis本身的机制执行分页sql
        return process(invocation, metaObject, pageNum, pageSize, getLimitSql(sourceSql));
    }

    @Override
    public Object plugin(Object target) {
        log.debug("[添加动态代理对象] interceptor: {}, target: {}", getClass().getSimpleName(), target.getClass().getSimpleName());
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        log.debug("Interceptor: {}, properties: {}", getClass(), properties);
        init(properties);
    }

    private void init(Properties properties) {
        log.debug("init: {}, properties: {}", getClass(), properties);

        String strDefaultPageNum = properties.getProperty("mybatis.assist.default.page.pageNum", "1");
        String strDefaultPageSize = properties.getProperty("mybatis.assist.default.page.pageSize", "12");
        String strDefaultUseFlag = properties.getProperty("mybatis.assist.default.page.useFlag", "true");
        String strDefaultCheckFlag = properties.getProperty("mybatis.assist.default.page.checkFlag", "false");
        String strDefaultCorrectPage = properties.getProperty("mybatis.assist.default.page.correctPage", "true");

        this.defaultPageNum = Integer.parseInt(strDefaultPageNum);
        this.defaultPageSize = Integer.parseInt(strDefaultPageSize);
        this.defaultUseFlag = Boolean.parseBoolean(strDefaultUseFlag);
        this.defaultCheckFlag = Boolean.parseBoolean(strDefaultCheckFlag);
        this.defaultCorrectPage = Boolean.parseBoolean(strDefaultCorrectPage);
    }

    protected abstract String getCountSql(String sourceSql);

    protected abstract String getLimitSql(String sourceSql);
}
